1<script setup lang="ts"> 2const config = useRuntimeConfig().public; 3const route = useRoute(); 4 5const { data: post } = await useAsyncData(`post-${route.path}`, () => 6 queryCollection("posts").path(route.path).first() 7); 8 9useSeoMeta({ 10 title: post.value?.title, 11 description: post.value?.description 12}); 13 14if (post.value) { 15 defineOgImageComponent("Post", { 16 title: post.value.title, 17 description: post.value.description, 18 date: post.value.date, 19 author: post.value.authors[0]?.name 20 }); 21} 22</script> 23 24<template> 25 <div 26 class="grow" 27 > 28 <NuxtLink 29 class="print:hidden flex items-center gap-1 text-sm text-neutral-600 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-100 mt-4" 30 to="/" 31 > 32 <Icon name="ri:arrow-drop-left-line" mode="svg" /> 33 Go back 34 </NuxtLink> 35 <article 36 v-if="post" 37 class="pb-24" 38 > 39 <header class="mb-8 mt-4"> 40 <h1 class="text-4xl font-bold"> 41 {{ post.title }} 42 </h1> 43 <div class="flex flex-wrap items-center justify-start gap-4 mt-4 text-neutral-600 dark:text-neutral-300"> 44 <div v-if="post.authors" class="flex items-center gap-1"> 45 <img v-if="post.authors[0]?.name === config.author" src="/logo.png" :alt="post.authors[0].name" 46 class="w-8 h-8 rounded-full mr-2"> 47 <span v-for="author in post.authors" :key="author.name"> 48 {{ author.name }} 49 </span> 50 </div> 51 · 52 <span> 53 {{ new Date(post.date).toLocaleDateString('en-GB', { 54 year: 'numeric', 55 month: 'long', 56 day: 'numeric' 57 }) }} 58 </span> 59 <span v-if="post.updated"> 60 <span class="text-neutral-500 dark:text-neutral-400 ml-2 mr-4">·</span> 61 <span class="text-sm"> 62 Updated: {{ new Date(post.updated).toLocaleDateString('en-GB', { 63 year: 'numeric', 64 month: 'long', 65 day: 'numeric' 66 }) }} 67 </span> 68 </span> 69 · 70 <div> 71 <span v-for="tag in post.tags" :key="tag" class="mr-2 mb-2 px-3 py-1 text-xs md:text-sm bg-stone-200 dark:bg-stone-700 text-stone-600 dark:text-stone-400 rounded-full"> 72 {{ tag }} 73 </span> 74 </div> 75 </div> 76 77 <div class="bg-stone-200 dark:bg-stone-700 h-[1px] my-4"></div> 78 79 <p 80 class="text-md text-neutral-500 dark:text-neutral-400 leading-7 my-8 text-justify md:w-[80%] mx-auto" 81 > 82 {{ post.description }} 83 </p> 84 </header> 85 86 <div class="bg-stone-200 dark:bg-stone-700 h-[1px] my-8 md:w-[80%] mx-auto"></div> 87 88 <TableOfContents v-if="config.tableOfContents" :post="post" /> 89 90 <ContentRenderer :value="post" class="post-body prose prose-lg leading-7 prose-slate dark:prose-invert text-justify md:w-[80%] mx-auto text-zinc-800 dark:text-zinc-200" /> 91 92 <ShareActions :title="post.title" :description="post.description" :author="post.authors[0]?.name" /> 93 94 <Suspense> 95 <BskyComments v-if="post.bskyCid" :cid="post.bskyCid" /> 96 97 <template #fallback> 98 <h1 class="md:w-[80%] mx-auto mt-16 text-xl font-bold text-stone-600">Loading comments...</h1> 99 </template> 100 </Suspense> 101 102 </article> 103 104 <div v-else class="flex items-center justify-center"> 105 <div class="text-center"> 106 <h1 class="text-4xl font-bold">404</h1> 107 <p class="text-neutral-500">Page not found</p> 108 </div> 109 </div> 110 </div> 111</template> 112 113<style> 114.post-body p:first-of-type::first-line { 115 font-weight: bold; 116} 117 118:target:before { 119 content: ""; 120 display: block; 121 height: 2rem; 122 margin: -2rem 0 0; 123} 124 125</style>